﻿// The MIT License (MIT)
//
// Copyright (c) 2015 FramTack, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
//
// For more information visit the project home page at http://solutionfamily.codeplex.com or the owner's site at http://solution-family.com
//

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Xml;

namespace SolutionFamily.IO
{
    public class J1939ParameterGroupFactory
    {
        private Dictionary<int, J1939ParameterGroup> m_pgns;
        private string m_definitionFile;

        public J1939ParameterGroupFactory()
        {
            m_pgns = new Dictionary<int, J1939ParameterGroup>();

            // check the exection assembly path
            var defs = Path.GetDirectoryName(LocalDevice.ExecutingAssemblyFullPath);
            defs = Path.Combine(defs, "PGNDefinitions.xml");

            if (!File.Exists(defs))
            {
                // look in the entry assembly path
                defs = Path.Combine(LocalDevice.RootPath, "PGNDefinitions.xml");
            }

            DefinitionFile = defs;
        }

        public J1939ParameterGroupFactory(string definitionFile)
        {
            DefinitionFile = definitionFile;

            m_pgns = new Dictionary<int, J1939ParameterGroup>();
        }

        public string DefinitionFile
        {
            get { return m_definitionFile; }
            private set
            {
                if (!File.Exists(value))
                {
                    throw new FileNotFoundException();
                }

                m_definitionFile = value;
            }
        }

        public bool SupportsPGN(int pgn)
        {
            switch (pgn)
            {
                case 65226: // DM1
                    return true;
                default:
                    return GetGroupForPGN(pgn) != null;
            }
        }

        public J1939ParameterGroup GetGroupDefinition(int pgn)
        {
            var group = GetGroupForPGN(pgn);

            if (group == null)
            {
                throw new NotSupportedException();
            }

            return group;
        }

        public J1939ParameterGroup ParseReceiveMessage(J1939ReceiveMessage message)
        {
            var group = GetGroupForPGN(message.ParameterGroupNumber);

            if (group == null)
            {
                throw new NotSupportedException();
            }

            group.PopulateParametersFromData(message.Data);
            return group;
        }

        private J1939ParameterGroup GetGroupForPGN(int pgn)
        {
            switch (pgn)
            {
                case 65226: // DM1
                    return new PGN65226();
                default:
                    // only return a copy, never the original (as we'll end up stuffing values into it)
                    if (m_pgns.ContainsKey(pgn))
                    {
                        return m_pgns[pgn].Copy();
                    }
                    else
                    {
                        var pg = GetParameterGroupFromDefinitionFile(pgn);

                        if (pg == null)
                        {
                            return null;
                        }

                        m_pgns.Add(pgn, pg);

                        return pg.Copy();
                    }
            }
        }

        private J1939ParameterGroup GetParameterGroupFromDefinitionFile(int pgn)
        {
            var pgnTest = pgn.ToString();
            var depth = 0;
            J1939ParameterGroup group = null;
            string attrib;

            using (var reader = new XmlTextReader(m_definitionFile))
            {
                while (reader.Read())
                {
                    // find the next ParameterGroup
                    if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "ParameterGroup"))
                    {
                        // is it the desired PGN?
                        var a = reader.GetAttribute("PGN");
                        if (a != pgnTest) continue;

                        var name = reader.GetAttribute("Name");
                        attrib = reader.GetAttribute("TransmissionRate");

                        int rate = -1;

                        if(attrib != null)
                        {
                            try
                            {
                                rate = int.Parse(attrib);
                            }
                            catch
                            {
                                // default to -1
                            }
                        }

                        // extract each parameter
                        depth = reader.Depth;

                        group = new J1939ParameterGroup(pgn, name, rate);

                        while (reader.Read())
                        {
                            // are we past all parameters?
                            if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "ParameterGroup"))
                            {
                                return group;
                            }

                            // are we past the last group?
                            if (reader.Depth < depth) return group;

                            if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "Parameter"))
                            {
                                J1939Parameter param = new J1939Parameter();

                                while (reader.MoveToNextAttribute())
                                {
                                    switch (reader.Name.ToLower())
                                    {
                                        case "spn":
                                            param.SPN = int.Parse(reader.Value);
                                            break;
                                        case "name":
                                            param.Name = reader.Value;
                                            break;
                                        case "units":
                                            param.Units = reader.Value;
                                            break;
                                        case "dataoffset":
                                            param.DataOffset = int.Parse(reader.Value);
                                            break;
                                        case "datalength":
                                            param.DataLength = int.Parse(reader.Value);
                                            break;
                                        case "scale":
                                            param.Scale = double.Parse(reader.Value);
                                            break;
                                        case "offset":
                                            param.Offset = double.Parse(reader.Value);
                                            break;
                                        case "datatype":
                                            switch (reader.Value.ToLower())
                                            {
                                                case "double":
                                                    param.DataType = typeof(double);
                                                    break;
                                                case "int":
                                                    param.DataType = typeof(int);
                                                    break;
                                                case "bool":
                                                    param.DataType = typeof(bool);
                                                    break;
                                                case "string":
                                                    param.DataType = typeof(string);
                                                    break;
                                                default:
                                                    throw new NotSupportedException(string.Format("Parameter data type '{0}' is not supported", reader.Value));
                                            }
                                            break;
                                        case "writable":
                                            param.Writable = Convert.ToBoolean(reader.Value);
                                            break;
                                        case "minvalue":
                                            param.MinValue = Convert.ToDouble(reader.Value);
                                            break;
                                        case "maxvalue":
                                            param.MaxValue = Convert.ToDouble(reader.Value);
                                            break;
                                    }
                                }

                                group.Parameters.Add(param);
                            }  // if Parameter
                        } // while Read
                    } // if ParameterGroup
                } // while Read
            } // using

            return group;
        }
    }
}
